智乐活

SSL 认证体系备忘录

2017/04/20 Share

本文不涉及SSL通信相关的知识,只介绍一下最近通过我的调研,了解到的关于SSL认证相关的东西。

  • JSSE Reference Guide
    一个关于SSL的比较专业的介绍,来自JSSE(Java Secure Socket Extension)

什么是密钥

要说证书,需要先说一下密钥是怎么回事。
目前的加密算法,分为对称加密和非对称加密两类,而密钥,可以把它想像成一把钥匙:

  • 对称加密: 使用同一把钥匙,即可实现对一段信息的加密和解密
  • 非对称加密:加密和解密信息,需要使用不同的钥匙

对称加密的优点就是速度快,所以可以用来加密大段的信息,但缺点也很明显,因为通信双方需要提前交换密钥,那么密钥本身就存在泄露的风险。

而非对称加密,由于加密和解密使用不同的密钥,所以只需要将其中一个密钥发送给对方(传说中的公钥),而另一个密钥由自己保管(也就是私钥),这样就避免了在传输环节丢失的风险。

不过,非对称加密算法非常复杂,所以性能比对称加密要低很多,而且对处理的信息长度有限制。所以在实际使用中,信息仍然以对称加密算法进行加密,而对称加密算法使用的密钥,由非对称加密算法进行加密传输。

常见的加密算法:

  • 对称加密:DES 3DES AES
  • 非对称加密:RSA DSA ECC

什么是签名

签名其实就是通过某种计算,将一段信息生成一个摘要,比如我们常见的MD5算法,就是干这个的。此外,常用的还有SHA算法。

签名并不是完美的

一般来讲,签名都是固定长度的,比如MD5生成的就是一个128位的二进制串。

不论源信息是一个字符,还是一整部小说,通过MD5算法生成的签名长度都是固定的,而固定长度的签名能表达的信息量是有限的,这也就导致了信息在转换过程中必定是有损不可逆的。

另一方面,由于签名只能表达有限个数的信息,而自然界中的信息数量则是无穷尽的,所以,必定导致不同的信息签名是会重复的。话虽如此,想要找到重复的签名并没有那么容易,比如MD5能够表达2^128种信息,想在这么多信息中找到一个重复的,想必也没那么容易。在术语中,将这种重复称之为冲突碰撞

什么是证书

证书本身和密钥是两个平行的概念。证书存在的目的,是对某个东西的认证。比如,你当前访问的网站的真实性、电子合同的有效性等等。而证书存在的形式,大体上就是由一个认证机构标识一个由它签发的签名组成。

认证机构——我们称之为CA (Certificate Authority)。只有受信的CA颁发的证书,才被信任。而一个CA是否被信任,则是由验证一方自己决定的。比如,操作系统会有一个受信的CA列表,在系统中的软件想要验证一个CA是否可信时,就可以把这个工作交给系统来完成。当然,也有自立门户的,比如JRE,自己有一个受信的CA列表,完全无视操作系统的CA认证。

一些浏览器,比如Chrome,也有自己的CA列表,不过他们应该是将自己的列表和系统的进行了一个合并。

这样就出现了如下情况:某些机构的证书在一个系统中好使,在另一个系统中不好使,或者不同浏览器表现也不一样。而Java从版本7开始,好似大幅减少了受信的CA列表,导致像沃通、StartSSL的证书都不在列表中,所以在Java7中进行SSL握手时,对果对方是这两个机构的证书的话,默认是不可信的,也就导致了握手失败。

沃通是国内做得比较大的CA机构,之前我们服务器的证书都是从沃通申请的;StartSSL是国际上用户量比较大的一个CA。

在某个时间,沃通收购了StartSSL,又在某个时间,Mozilla宣布暂时将沃通和StartSSL移出受信列表,原因是他们使用不再安全的SHA1算法且恶意欺骗浏览器,来龙动脉可以关注知乎话题 如何看待 Mozilla 决定停止信任沃通 (WoSign) 和 StartCom 颁发的证书?

后来,Safari和Chrome也跟随Mozilla取消了沃通和StartSSL的受信。不过,据说再后来的版本已经相继又恢复了这两个CA的受信。江湖实在险恶,我等凡夫俗子看个热闹就好。

我们的服务器现在用的是LetsEncrypt颁发的证书,通过测试,这个证书在各类客户端中兼容性都还不错,不过在Java7中还是不认识这个证书。

如何证明“我就是我”

刚才说的那套证书认证体系,要想工作得很好,就必须解决两个问题:

  1. 如何知道一个证书就是由某个CA签发的,而不是由别人伪造的
  2. 如何验证某个证书所认证的内容是否发生了改变

解决第1个问题

这就要用到之前说的非对称加密技术了,流程大概是这个样子的:

  1. CA生成自己的一套证书,其中包含公钥私钥
  2. CA将私钥放进自己的保险库,并雇佣黑帮7*24小时看守
  3. CA将公钥公之于众
  4. 验证者通过公开信息获取到这个CA的公钥,并将此CA列入受信列表
  5. CA用自己的私钥签发一个证书
  6. 验证者拿到证书后,根据证书上的标识找到这个CA,发现其在自己的受信列表中,于是找到这个CA的公钥,并使用公钥来尝试解密证书内容
  7. 如果解密成功,则证书验证通过,否则验证失败

解决第2个问题

这就要用到之前说的签名了。

每个证书除了包含对自己的身份验证信息外,还包含了对所认证内容的签名。验证者将需要验证的内容以相同的算法做签名,然后将此签名和证书上记录的签名做对比,如果签名相同,则表示内容无误,否则说明内容已经被修改了。

证书链(Certificate Chain)

在实际使用过程中,一个SSL证书很可能是多级认证的,也就是说A机构认证了B,B又认证了C,而证书最终是由C签发的。一般来讲,在这整个环节中,只有A是受信的CA,而B和C有可能是A自己生成的分级证书,或是A的合作机构。

下面是一张画得非常棒的图示,很好的说明了证书链的工作模式:
证书链的工作模式
图片来自 http://blog.csdn.net/shen_guo/article/details/49891459

可以使用openssl命令来查看某个网站的证书信息:
openssl查看某网站的证书

其中的0/1/2就是证书链信息。

解决Java高版本中的证书问题

前文提到,Java从版本7开始,取消了大量的受信CA,很不幸我们自己的证书CA(LetsEncrypt)也被取消了。好在各种前端系统还是认我们的证书的,所以这只是对我们后台程序之间互相通信产生了影响。

那如何让Java信任我们的CA呢?就是将我们的CA手动加入到信任列表中。有两种办法可以做到:

  1. 将CA根证书导入Java的受信CA列表,这样所有的Java程序都将信任此CA,但缺点是需要在所有机器上进行手动导入。
  2. 在程序中将CA根证书加入信任列表,这样只有在当前会话中,该CA才被认可,不对其它程序产生影响,且无需手动修改机器配置。

我决定使用第2种方式来实现我们的需求。

zlhcommon库中,包com.zhilehuo.server.zlhcommon.ssl下面有两个类:CompositeX509TrustManagerCompositeX509TrustManagerAgent。其中,CompositeX509TrustManagerAgentCompositeX509TrustManager的基础上,可以快速将沃通、StartSSL、LetsEncrypt证书添加至受信列表,也可以方便的将其它证书添加至受信列表。

此外,com.zhilehuo.server.zlhcommon.httptools.ZlhHttpClientBuilder这个类,可以快速生成基于CompositeX509TrustManagerAgentCloseableHttpClient,并结合SimpleRequest类,进行HTTPS请求。

Java中自定义受信CA的坑

  • 默认情况下,Java在设置了自定义CA后,系统就不再认识原有的CA列表,这就要求我们在请求之前,先判断目标站点是否是受信站点,如果不是,再加载我们自己的证书,这样比较麻烦
  • 默认情况下,自定义CA每次只允许设置一个证书文件,无法同时将多个CA证书导入,这样实际情况下也会遇到一些麻烦

所以,我们才在zlhcommon库中,集成了CompositeX509TrustManager类,来解决这些问题。

CATALOG
  1. 1. 什么是密钥
  2. 2. 什么是签名
  3. 3. 什么是证书
  4. 4. 如何证明“我就是我”
    1. 4.1. 解决第1个问题
    2. 4.2. 解决第2个问题
  5. 5. 证书链(Certificate Chain)
  6. 6. 解决Java高版本中的证书问题